重点介绍如何减少组件的重渲染次数
React 的更新机制
JSX 的本质就是 虚拟 DOM,组件在挂载阶段,React 底层会将 虚拟 DOM 转换为 真实 DOM(commit)渲染到页面中。
React 使用虚拟 DOM 的优势在组件挂载阶段并没有体现出来,可以说比直接使用真实 DOM 的效率还要低一些,因为多了虚拟 DOM 到真实 DOM 的这一层转换。不过虚拟 DOM 的优势在于后续页面的更新上,如果更新的元素内容比较少,它可以实现精准的定量更新,不需要把全部的 DOM 元素删除重添加。
当组件进入更新阶段就会返回 新的虚拟 DOM,新的虚拟 DOM 会与 旧的虚拟 DOM 通过 diff 算法 来进行比较,保存 两棵虚拟 DOM 树的差异 ( patch ),最后,React 会根据 两棵虚拟 DOM 树的差异 来精准更新部分真实 DOM。
React 性能优化的关键就是减少虚拟 DOM 的生成从而减少新旧虚拟 DOM 的 diff,简单来说就是减少组件 render 的次数,即减少新的虚拟 DOM 的生成次数。
important
类组件的 render 就是说执行类组件中的 render 方法,函数组件的 render 就是说重新执行这个函数。
在默认情况下,只要当前组件的父组件进行了重新渲染,或者当前组件调用了 setState,那么该组件就会进入更新阶段,并且还需要注意的是如果该生成了新的虚拟 DOM,那么其内部的所有子组件都会进入更新阶段。
import React, { Component } from "react";
class Child extends Component {
render() {
return <div>Child</div>;
}
}
export default class Father extends Component {
state = { count: 0 };
render() {
return (
<div>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
{this.state.count}
</button>
<Child />
</div>
);
}
}
当我们点击按钮后,当前 Father 组件会进入组件的更新阶段,其内部的所有子组件也都会进入更新阶段。但是对于 Child 组件,它的状态和 props 都没有发生变化,但是依旧重新执行的 render 方法。
只要组件执行了 render 方法,就会返回新的虚拟 DOM 树,那么就会进行 diff 比较,这个过程还是比较耗时的。
我们可以使用下面的这些方案来减少 render 执行次数。
shouldComponentUpdate
shouldComponentUpdate(nextProps, nextState) {}
简称 SCU,是类组件的一个生命周期函数,类组件进入更新阶段后,会接收 nextProps
和 nextState
即组件进入更新阶段所接收到的 props
和 state
。默认会返回 true
。
- 该方法如果返回
true
则类组件就会执行render
方法产生新的虚拟 DOM - 返回
false
则阻断后续生命周期函数的执行
如果类组件是通过 forceUpdate
来让组件进入更新阶段的,那么就会跳过 SCU,直接执行 render
方法。
在类组件的性能优化方案中,我们可以重写它的 SCU,让 nextProps
和 nextState
分别与 this.props
和 this.state
进行比较,如果没有发生变化则直接阻断后续生命周期函数的执行。
在下面这个例子中,我们没有重写 SCU,默认情况下,当我们点击按钮时 Father 组件就会进入更新阶段,当执行到 render
方法时,还会导致其内部的所有子组件也进入更新阶段。
import React, { Component } from "react";
class Child extends Component {
render() {
return <div>Child</div>;
}
}
export default class Father extends Component {
state = { count: 0 };
render() {
return (
<div>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
{this.state.count}
</button>
<Child name="child" />
</div>
);
}
}
可以很明显的发现,Child
组件的 props
没有变化,并且 state
也没有变化,但是它会进入更新阶段,重新调用 render
返回新的虚拟 DOM。这个过程完全是没有必要的。因此我们重写它的 SCU,在它进入更新阶段时,如果它的 props
和 state
没有发生变化,则不执行 render
。
class Child extends Component {
shouldComponentUpdate(nextProps, nextState) {
if (nextProps.name === this.props.name && nextState === this.state) return false;
return true;
}
render() {
return <div>Child</div>;
}
}
important
- 只要组件重新执行了
render
,其内部所有子组件的props
都会变为一个新的引用 - React 官方是不建议在 SCU 中对 props 或 state 进行深层比较的,因为这样的效率非常低
React.PureComponent
先说一下 React.Component
的缺点:
- 底层没有实现
shouldComponentUpdade
,默认返回 true - 无论父组件传来的 props 是否发生变化,只要父组件在更新阶段执行了 render,那么本组件也会进入更新阶段
- 调用 setState 时,无论是否改变状态,都会使组件进入更新阶段,比如传入一个空对象 ( 不会改变状态 )
而 React.PureComponent
通过浅层比较 props
和 state
的方式实现了 shouldComponentUpdate
,这里直接看它的源码。
function checkShouldComponentUpdate(
workInProgress,
ctor,
oldProps,
newProps,
oldState,
newState,
nextContext,
) {
// removed some code
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return (
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
return true;
}
function shallowEqual(objA, objB) {
// 两个对象引用相等则返回 true
if (is(objA, objB)) {
return true;
}
// 只要有其中一个不是对象或者是 null 则返回 false
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
// 获取两个对象的 key
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
// key 数量不一样则返回 false
if (keysA.length !== keysB.length) {
return false;
}
// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
if (
!hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false;
}
}
return true;
}
源码中的 is
本质上就是 Object.is
,需要注意的是,它的作用不仅仅是比较两个对象的引用是否一致,并且还能比较基本数据类型。
const obj1 = {};
const obj2 = {};
const num1 = 1;
const num2 = 2;
Object.is(obj1, obj2); // false
Object.is(num1, num2); // false
Object.is(null, null); // true
- 对于引用数据类型来说,比较的引用是否相同
- 对于基本数据类型来说,比较的是变量值是否相同
React.PureComponent
中的关键代码就是对 props
和 state
进行了浅层比较。
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
因此使用 React.PureComponent
时,尽量保证 props
和 state
对象上的属性均为基本数据类型,这样在浅层比较时才能够准确的判断 props
和 state
是否发生改变。
如果 props
和 state
对象上的属性存在引用数据类型,那么只会比较该属性第一层所取的引用值。
React.memo
React.memo(MyComponent[, areEqual])
这是一个高阶组件,主要用于 函数组件 性能优化,需要注意的是它只会浅层比较 props,原理如下:
- 当该组件进入更新阶段,会将
preProps
和nextProps
进行浅层比较,如果相等则不会 rerender 该函数组件
该高阶组件还有一个 areEqual
参数,默认情况下底层通过上面的方式实现了 areEqual
函数,但是我们可以重写这个函数。
function areEqual(prevProps, nextProps) {
/*
1. 在里面可以接受到当前的 props 与父组件传来的 props
2. 返回 true 则说明相等,反之则不相等
*/
}